#!/usr/bin/env python
# -*- mode: python; coding: utf-8; -*-
# Copyright 1998-2003 Markus Oberhumer <markus@oberhumer.com>
# Copyright 2003 Mt. Hood Playing Card Co.
# Copyright 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
# Copyright 2010- Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
#
# This file is part of Pysol-UP.
#
# Pysol-UP is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License version 3 of the License as
# published by the Free Software Foundation.
#
# Pysol-UP is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Pysol-UP. If not, see <http://www.gnu.org/licenses/>.
"""
Parse pack details and mange card/tile sets.
"""
__docformat__ = 'restructuredtext en'

import os
import logging

LOGGER = logging.getLogger('PySolFC.Packlib.CardData')

from . import info as CSI
from . import group as CSG
from .image import Images, Size, Offset
from ..resourceng import ResourceManager, Resource, SearchDirs
from ..settings import CONF_OPT
from ..viewlib.dialog import Dialog


class PackDetailError(Exception):
    """
    Pack Detail Error class.
    """

    def __init__(self, value=None):
        """
        Pack detail error handler for format issues.

        Given ``value`` with key, line and field raise appropriate message. The
        given string must start with the error key, then optional space seperate
        line and field. *NUM! 2 1* would be non-digit value on line 2, field 1.

        **Error key**:
          ``LIN#`` - Incorrect number of lines.
          ``FLD#`` - Incorrect number of fields.
          ``FMT!`` - Incorrect format in general.
          ``NUM!`` - Field with more than digits.
          ``RNG!`` - Field with value out of range.
          ``EXT!`` - Field with unknown extension.
          ``FLD!`` - Field missing.

        \__init__(self, value=None:str) => None

        """
        if value is None:
            message = 'Unknown error.'
        else:
            value = str(value).split()
            message = {'LIN#': '{}\nIncorrect number of lines.',
                       'FLD#': '{}\nIncorrect number of fields.',
                       'FMT!': '{}\nIncorrect format.',
                       'NUM!': '{}\nField must be a number.',
                       'RNG!': '{}\nField must be in range.',
                       'EXT!': '{}\nField must be an image extension.',
                       'FLD!': '{}\nField missing.',
                       }.get(value[0], '{}\nGeneral error.')
            message = ("line #{0[1]}, field #{0[2]}" if len(value) > 2
                                              else "line #{0[1]}").format(value)
        super(PackDetailError, self).__init__('Pack Detail Error: {}'
                                              '\n{}'.format(message))


def _verify(expression, error):
    """
    Verify given ``expression`` else raise ``PackDetailError`` with given
    ``error`` message key.

    .. `_verify(expression:str, error:str) -> None
    """
    if not expression:
        raise PackDetailError(error)


class _ParseCardset(object):
    """
    Cardset(Resource)

    Rseource containing details of an available cardset.
    """
    __parsefields = lambda s, i: [f.strip() for f in
                                            s.__lines[i].split(';;  ;;'[i])]

    def __init__(self, path):
        """
        Initialize PySol card detail parser.

        Load each line of the card detail file found within ``path``.

        .. \__init__(self, path:str) -> None

        *Parameters:*
          ``path``:
            Pack dir to read detail file from.

        *Raises*:
           Error
        """
        self.__pack = {}
        self.__path = path

        with open(os.path.join(self.__path, "config.txt"), "r") as file_:
            self.__lines = file_.readlines()
        if len(self.__lines) < 6:
            raise PackDetailError('LIN# 1')

    def parse(self):
        """
        Return pack details.

        Call detail, dimmension, and design methods to parse each section.

        .. parse() => [None|dict]
        """
        LOGGER.debug('LINE 1: %s', self.__lines[0])
        LOGGER.debug('LINE 2: %s', self.__lines[1])
        self.__details()
        LOGGER.debug('LINE 3: %s', self.__lines[2])
        LOGGER.debug('LINE 4: %s', self.__lines[3])
        self.__dimmensions()
        LOGGER.debug('LINE 5: %s', self.__lines[4])
        LOGGER.debug('LINE 6: %s', self.__lines[5])
        self.__designs()
        LOGGER.debug('%s', self.__pack)
        return self.__pack

    __attributes = {
            CSI.TYPE_FRENCH: {
                'ranks': list(range(13)), 'suits': 'cshd'},
            CSI.TYPE_HANAFUDA: {
                'ranks': list(range(4)), 'suits': 'abcdefghijkl',
                'nbottoms': 15},
            CSI.TYPE_TAROCK: {
                'ranks': list(range(14)), 'suits': 'cshd', 'trumps': list(range(22)),
                'nbottoms': 8},
            CSI.TYPE_MAHJONGG: {
                'ranks': list(range(10)), 'suits': 'abc', 'trumps': list(range(12)),
                'nbottoms': 0, 'nletters': 0, 'nshadows': 0},
            CSI.TYPE_HEXADECK: {
                'ranks': list(range(16)), 'suits': 'cshd', 'trumps': list(range(4)),
                'nbottoms': 8},
            CSI.TYPE_MUGHAL_GANJIFA: {
                'ranks': list(range(12)), 'suits': 'abcdefgh', 'nbottoms': 11},
            CSI.TYPE_NAVAGRAHA_GANJIFA: {
                'ranks': list(range(12)), 'suits': 'abcdefghi', 'nbottoms': 12},
            CSI.TYPE_DASHAVATARA_GANJIFA: {
                'ranks': list(range(12)), 'suits': 'abcdefghij', 'nbottoms': 13},
            }

    def __details(self):
        """
        Parse the details of pack details.

        .. \__details() -> None

        Each line contains semicolon seperated fields.
          line 1 fields:
            #. ``PySol``; - Format flag. ver >= 3
            #. {number}; - Format version. ver >= 3
            #. {text}; - Image extension. ver >= 3
            #. {number}; - Pack type. ver >= 3
            #. {number}; - Pack count. ver >= 3
            #. {sequence(number, ...)}; - Pack styles; ver >= 4
            #. {number} - Pack year. ver >= 5

          line 2 fields:
            #. {text}; - Pack UID (``ID-YEAR-NAME``). ver >= 1
            #. {text} - Pack title. ver >= 1

        *Raises*:
          ``PackDetailError``:
            When detail syntax incorrect.
        """
        from ..settings import IMAGE_EXTENSIONS

        pack = self.__pack
        pack.update({  # line[0]
                       'version': 1, 'ext': '.gif', 'type': CSI.TYPE_FRENCH,
                       'ncards': -1, 'styles': set(), 'year': 0,
                       #line[1]'
                       'ident': '', 'name': ''})

        fields = self.__parsefields(0)
        _verify(self.__lines[0].startswith("PySol"), 'FMT! 1')

        if len(fields) >= 2 and fields[1].isdigit():
            pack['version'] = int(fields[1])

        if pack['version'] >= 3:
            _verify(len(fields) >= 5, 'FLD# 1')
            _verify(fields[2] in IMAGE_EXTENSIONS, 'EXT! 1')
            pack['ext'] = fields[2]
            _verify(fields[3].isdigit(), 'INT! 1 3')
            _verify(int(fields[3]) in CSG.TYPE, 'RNG! 1 3')
            pack['type'] = int(fields[3])
            if pack['type'] in self.__attributes:
                pack.update(self.__attributes.get(pack['type'], {}))
            _verify(fields[4].isdigit(), 'INT! 1 4')
            pack['ncards'] = int(fields[4])

        if pack['version'] >= 4:
            _verify(len(fields) >= 6, 'FLD# 1')
            for style in (f.strip() for f in fields[5].split(",")):
                _verify(style.isdigit(), 'INT! 1 5')
                pack['styles'].add(int(style))  # use set instead of check each

        if pack['version'] >= 5:
            _verify(len(fields) >= 7, 'FLD# 1')
            _verify(fields[6].isdigit(), 'INT! 1 6')
            pack['year'] = int(fields[6])

        _verify(self.__lines[1], 'LIN! 2')
        fields = self.__parsefields(1)
        _verify(len(fields) == 2, 'FLD# 2')
        pack['ident'] = fields[0]
        pack['name'] = fields[1]

    def __dimmensions(self):
        """
        Parse the card/tile dimmensions of pack details.

        .. \__dimmensions(self) -> None

        Each line contains space seperated fields.
          line 3 fields:
            #. {number} - Card/Tile width.
            #. {number} - Card/Tile height.
            #. {number} - Image color depth.

          line 4 fields:
            #. {number} Card/Tile x-offset.
            1) {number} Card/Tile y-offset.
            2) {number} Shadow x-offset.
            3) {number} Shadow y-offset.

        *Raises*:
          ``PackDetailError``:
            When detail syntax incorrect.
        """
        pack = self.__pack
        pack.update({ # line[2]
                      'size': Size(0, 0), 'depth': 0,
                      # line[3]
                      'offset': Offset(0, 0), 'shadow': Offset(0, 0)})

        def verify_fields(line, fields):
            """
            Verify fields contain only digits.
            """
            for index, value in enumerate(fields):
                _verify(value.isdigit, 'INT! {} {}'.format(line, index))

        fields = self.__parsefields(2)
        _verify(len(fields) == 3, 'FLD# 3')
        verify_fields(3, fields)
        pack['size'] = Size(int(fields[0]), int(fields[1]))
        pack['depth'] = int(fields[2])

        fields = self.__parsefields(3)
        _verify(len(fields) == 4, 'FLD# 4')
        verify_fields(4, fields)
        pack['offset'] = Offset(int(fields[0]), int(fields[1]))
        pack['shadow'] = Offset(int(fields[2]), int(fields[3]))


    def __designs(self):
        """
        Parse the back designs of pack details.

        .. \__dimmensions(self) => None

        Each line contains semicolon seperated fields.
          line 5 fields:
            #. {text} - Default card/tile back.

          line 6 fields:
            sequence of available card backs.

        *Raises*:
          ``PackDetailError``:
            When detail syntax incorrect.
        """
        pack = self.__pack
        pack.update({  # line[4]
                       'backindex': 0,
                       # line[5]
                       'backnames': ()})

        back = self.__parsefields(4)[0]
        _verify(back, 'FLD! 5 1')

        pack['backnames'] = self.__parsefields(5)

        if back in pack['backnames']:
            pack['backindex'] = pack['backnames'].index(back)
        else:
            pack['backnames'].insert(0, back)


class Cardset(Resource):
    """
    Cardset(Resource)

    Resource containing details of an available cardset.
    """

    def __init__(self, **kwargs):
        """
        Set allowed attributes and their default values.
        """
        self._defaults(kwargs,
                # essentials
                ranks=(), suits='', trumps=(),
                nbottoms=7, nletters=4, nshadows=1 + 13,
                # implicit
                backname=None)
        # Set trump only pack to sequence of ncards field
        if kwargs['type'] == CSI.TYPE_TRUMP_ONLY:
            kwargs.update({'trumps': list(range(kwargs['ncards'])),
                           'nbottoms': 1, 'nletters': 0, 'nshadows': 0})
        # selection criterias queried by select cardset" dialog
        kwargs.update({'si': {'type': 0, 'size': 0, 'styles': [],
                              'nationalities': [], 'dates': []}})
        super(Cardset, self).__init__(**kwargs)

    def update_back(self, name=None, index=None):
        """
        Update default card back design.

        Set back design with either given ``name`` or ``index``. When
        neither set use current backindex field value.

        .. update_back(name=None | index=None) -> None

        *Parameters:*
          ``backindex`` : {number}
            Index of back design to set.

          ``backname`` : {text}
            Name of back design to set.
        """
        if isinstance(name, str) and name in self['backnames']:
            index = self['backnames'].index(name)
        if isinstance(index, int):
            self['backindex'] = index % len(self['backnames'])
        self['backname'] = self['backnames'][self['backindex']]


class CardsetManager(ResourceManager):
    """
    CardsetManager(ResourceManager)

    Resource Manager containing available cardset details.
    """
    _depth = 24

    def __init__(self):
        """
        __init__(self) => None
        """
        super(CardsetManager, self).__init__()
        self._imageset = None

#        #self.registered_types = {}
#        #self.registered_sizes = {}
#        #self.registered_styles = {}
#        #self.registered_nationalities = {}
#        #self.registered_dates = {}

    def register(self, resource):
        """
        Register pack resource detail.

        Overide to update given ``resource``. Shorten name field to 25 chars.
        Add selection info for grouping similar resources by size, style,
        nationality, and date. Call resource update back method. Final call next
        method in order with updated ``resource``.

        .. register(self, resource:Cardset) => None

        *Parameters:*
          ``resource``: {mapping}
            Details of a card/tile pack to be added.
        """
        count = (len(resource['ranks']) * len(resource['suits']) +
                 len(resource['trumps']))
        if not resource['ncards'] == count:
            if resource['version'] > 3:
                resource['ncards'] = count
            else:
                LOGGER.warning('%s: ncards field is incorrect. (%s, %s)',
                               resource['dir'], resource['ncards'], count)

        resource['name'] = resource['name'][:25]

        resource['si']['type'] = resource['type']

        # set card size and spacers
        for value, size in [(CSI.SIZE_TINY, (55, 72,),),
                            (CSI.SIZE_SMALL, (60, 85,),),
                            (CSI.SIZE_MEDIUM, (75, 105,)),
                            (CSI.SIZE_LARGE, (90, 125,),)]:
            if all(c <= d for c, d in zip(resource['size'], size)):
                resource['si']['size'] = value
                break
        else:
            resource['si']['size'] = CSI.SIZE_XLARGE

        # set card style
        setstyle = lambda t: [s for s in resource['styles'] if s in t]
        resource['si']['styles'] = setstyle(CSG.STYLE)
        resource['si']['nationalities'] = setstyle(CSG.NATIONALITY)
        resource['si']['dates'] = setstyle(CSG.DATE)

        resource.update_back()
        super(CardsetManager, self).register(resource)

    def load(self):
        """
        Load pack resource detail.

        Call search dir method with default pack paths and environment variable.
        Verify resource parse success, resource color depth with given
        ``depth``, and resource back designs exist. When verified, call
        register method of class.

        .. load(self, depth:int) -> None

        *Parameters:*
          ``depth``:
            Color depth of screen.
        """
        isfile_ = lambda p, i: os.path.isfile(os.path.join(p, i))

        for dir_ in SearchDirs(["cardsets"], "PYSOL_CARDSETS"):
            names = []
            if dir_ and os.path.isdir(dir_):
                names = os.listdir(dir_)
                names.sort()
            for name in (n for n in names if n.startswith('cardset-')):
                path = os.path.join(dir_, name)
                if not os.path.isdir(path) or not isfile_(path, "COPYRIGHT"):
                    continue
                try:
                    pack = _ParseCardset(path).parse()
                except PackDetailError as error:
                    LOGGER.warning('%s: %s', path, error)
                pack['dir'] = path
                name = pack['name']
                # verify color depth
                if pack['depth'] >= self._depth:
                    LOGGER.warning('%s detail color depth too high. %s>=%s',
                                    name, pack['depth'], self._depth)
                    continue
                del pack['depth']
                # verify back names
                backs = [i for i in pack['backnames'] if not isfile_(path, i)]
                if backs:
                    LOGGER.warning('%s missing image for %s.', backs, name)
                    continue
                self.register(Cardset(**pack))

    @property
    def current_imageset(self):
        """
        Return current imageset
        """
        return self._imageset

    def update_imageset(self, update=7):
        """
        Update imageset  according to given ``update`` value. Sets bottom
        image dark/light based on negative bottom config option.

        .. update_imageset([update:int=7]) -> None

        *Parameters*:
          ``update`` : {number} 7
            Flag to indicate cardset update for which variables.

            ``1`` : Set default cardset for all games.

            ``2`` : Set default cardset for cardset type

            ``7`` : Set default cardset for cardset type and for all games.

        *Requires*:
          Methods:
            ``..images.Images.bottom_light``.
             ``Cardset.choice`` - Set current imageset.
          Variables:
            ``..settings.CONF_OPT['pile_bottom']`` - Set its value as
            bottom image dark/light.

            ``..settings.CONF_OPT['cardset']['*category*]`` - Set default ``0``
            keyword or *category** keyword based on given ``update`` value.

        *Raises*:
           AssertionError:
             When not ``Cardset._imageset.detail['index']``.
        """
        assert self._imageset and self._imageset.detail['index']
        detail = self._imageset.detail
        LOGGER.info('update pack %s', detail['name'])
        # update settings
        self.choice(detail['index'])
        self._imageset.bottom_light(CONF_OPT['pile_bottom'])
        # update options
        pack_choice = (detail['name'], detail['backname'])
        if update & 1:
            CONF_OPT['cardset'][0] = pack_choice
        if update & 2:
            CONF_OPT['cardset'][detail['type']] = pack_choice

    def _load_imageset(self, detail, progress=None):
        """
        Load new image set for current detail.

        When given detail does not match current imageset detail, Confirm given
        detail image set loads and set new image set. Returns success load
        imageset.

        .. \__new_set(detail:CardsetInfo[, progress:Progress=None) -> bool

        *Parameters*:
          ``detail`` : {PackDetail}
            Init ``Images`` with given value and load new imageset.
          ``progress``: {Progess} None
             When given value is not none, update progress.

        *Methods:*
          ``..packlib.images.Images.load``; ``self.choice``.

        *Variables:*
          ``self._imageset`` - Set value to new image set.

          ``..settings.CONF_OPT[`games_geometery']`` - Reset mapping.

        *Raises:*
          AssertionError:
            When not ``detail['index']``.
        """
        assert detail and not detail['error']
        # Current cardset
        if self.current_imageset and detail is self.current_imageset.detail:
            LOGGER.info('pack %s from cache.', detail['name'])
            return True

        # Different cardset
        LOGGER.info('pack %s from file.', detail['name'])
        imageset = Images(detail)
        if imageset.load(progress=progress):
            self._imageset = imageset
            return True

        # Failure
        Dialog.error(text=_('Error loading pack: {}.').format(CSI.CARDSET))
        detail['error'] = True
        if self.current_imageset:
            self.choice(self.current_imageset.detail['index'])
        return False

    def request_imageset(self, pack_type, progress=None):
        """
        Request new imagset for pack detail.

        Verify pack detail with type required for game and when valid call load
        imageset method with detail. Try current choice then current imageset
        and final config option. Return sucess load valid imageset.

        .. request_imageset(pack_type:int[, progress:Progress=None) -> None

        Parameters:
          ``pack_type`` : {number}
            Value to indicate pack_type requested by game.
          ``progress``: {Progess} None
             When given value is not none, update progress.

        *Methods:*
          ``.info.verify_cardset``; ``self._load_imageset``.

        *Variables:*
          ``self.current`` - 1st choice for imageset.

          ``self.current_imageset`` - 2nd choice for imageset.

          ``..settings.CONF_OPT['carset']['*type*]`` - 3rd choice for imageset.
        """
        verify = lambda c: CSI.verify_cardset(c['type'], pack_type)

        def load(detail):
            """
            Verify and load imageset for detail.
            """
            if not self.current['error'] and verify(detail):
                return self._load_imageset(detail, progress=progress)

        # Load imageset from choice or current imageset.
        imageset = self.current_imageset
        if load(self.current) or (imageset and verify(imageset.detail)):
            self.update_imageset()
            return True

        # Load imageset from from config options.
        name, back = CONF_OPT['cardset'].get(pack_type)
        if name in self and load(self.find(name)):
            self.current.update_back(name=back)
            self.update_imageset(update=1)
            return True
        return False

CARD_DB = CardsetManager()